﻿using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;

namespace SIToFb2
{
  public class MainFormView : INotifyPropertyChanged
  {
    private double _progress;
    private readonly SynchronizationContext _syncContext;
    private bool _isIdle;

    private const string ExampleUrlStr = "example: http://zhurnal.lib.ru/p/pupkin_wasja_ibragimowich/niktxt.shtml";
    private readonly string[] _startLines = new[]
                                          {
                                            "<!----------- Собственно произведение --------------->",
                                          };

    private readonly string[] _endLines = new[]
                                          {
                                            "<!--------------------------------------------------->",
                                          };

    private string _pagePath;
    public string PagePath
    {
      get { return _pagePath; }
      set
      {
        if (_pagePath == value) return;
        _pagePath = value;
        OnPropertyChanged("PagePath");
      }
    }

    private string _savePath;
    public string SavePath
    {
      get { return _savePath; }
      set
      {
        if (_savePath == value) return;
        _savePath = value;
        OnPropertyChanged("SavePath");
      }
    }

    private string _chaptersRegexText;
    public string ChaptersRegexText
    {
      get { return _chaptersRegexText; }
      set
      {
        if (_chaptersRegexText == value) return;
        _chaptersRegexText = value;
        OnPropertyChanged("ChaptersRegexText");
      }
    }

    private bool _useChaptersRegex;
    public bool UseChaptersRegex
    {
      get { return _useChaptersRegex; }
      set
      {
        if (_useChaptersRegex == value) return;
        _useChaptersRegex = value;
        OnPropertyChanged("UseChaptersRegex");
      }

    }

    private string _partsRegexText;
    public string PartsRegexText
    {
      get { return _partsRegexText; }
      set
      {
        if (_partsRegexText == value) return;
        _partsRegexText = value;
        OnPropertyChanged("PartsRegexText");
      }
    }

    private bool _usePartsRegex;
    public bool UsePartsRegex
    {
      get { return _usePartsRegex; }
      set
      {
        if (_usePartsRegex == value) return;
        _usePartsRegex = value;
        OnPropertyChanged("UsePartsRegex");
      }

    }

    public double Progress
    {
      get { return _progress; }
      set
      {
        if (_progress == value) return;
        _progress = value;
        OnPropertyChanged("Progress");
      }
    }

    public MessageCollection Messages { get; set; }

    public bool IsIdle
    {
      get { return _isIdle; }
      set
      {
        if (_isIdle == value) return;
        _isIdle = value;
        OnPropertyChanged("IsIdle");
      }
    }

    public MainFormView()
    {
      Messages = new MessageCollection();
      PagePath = ExampleUrlStr;
      SavePath = "niktxt.fb2";
      ChaptersRegexText = @"(.*глава\s[0-9]+)|(.*пролог)";
      PartsRegexText = @".*часть\s[0-9]+";
      UseChaptersRegex = true;
      UsePartsRegex = true;
      IsIdle = true;
      _syncContext = SynchronizationContext.Current;
    }

    public string GetSelectedFileName()
    {
      var selectedFileName = PagePath;
      if (string.IsNullOrEmpty(selectedFileName)) return string.Empty;
      var strings = selectedFileName.Trim().Split(new[] { '\\', '/' });
      return strings.LastOrDefault();
    }

    public void Transform()
    {
      IsIdle = false;
      try
      {
        Messages.Clear();
        Messages.Notification("Start converted");
        ParsedFicionBook book = ParseHtml();
        XmlDocument xmlDoc = CreateFB2Document(book);
        xmlDoc.Save(SavePath);
        Progress = 100;
        Messages.Notification("File converted");
      }
      catch (Exception e)
      {
        Messages.Error(e.Message);
      }
      IsIdle = true;
    }

    private ParsedFicionBook ParseHtml()
    {
      ParsedFicionBook book = new ParsedFicionBook();
      using (var reader = new StreamReader(PagePath, Encoding.GetEncoding(1251)))
      {
        var allLinesCount = CountLines(reader);
        reader.BaseStream.Position = 0;
        var chaptersRegex = new Regex(ChaptersRegexText, RegexOptions.Compiled | RegexOptions.IgnoreCase);
        var partsRegex = new Regex(PartsRegexText, RegexOptions.Compiled | RegexOptions.IgnoreCase);
        var htmlRegex = new Regex("<.*?>", RegexOptions.Compiled);
        string line;
        var foundStart = false;
        var lineCounter = 0;
        var currentChapter = -1;
        Part currentPart = null;
        //        currentPart.Chapters.Add(new Chapter { Lines = { "<section>" } });
        //        book.Parts.Add(currentPart);
        while ((line = reader.ReadLine()) != null)
        {
          Progress = ++lineCounter * 74.0 / allLinesCount;
          if (string.IsNullOrEmpty(line)) continue;
          if (line.Contains("<title>") && line.Contains("</title>"))
            book.Title = line.Replace("<title>", string.Empty).Replace("</title>", string.Empty).Trim();

          if (line.Contains(_startLines[0]))
          {
            foundStart = true;
            continue;
          }

          if (line.Contains("Copyright"))
          {
            var authStart = line.IndexOf("<a");
            var authorText = line.Substring(authStart);

            var authorTextCleared = htmlRegex.Replace(authorText, string.Empty);
            var splited = authorTextCleared.Split(new[] { ' ' });

            if (splited.Length >= 3)
              book.Middle = splited[2];
            if (splited.Length >= 2)
              book.First = splited[1];
            if (splited.Length >= 1)
              book.Last = splited[0];
          }

          if (!foundStart) continue;

          if (line.Contains(_endLines[0]))
          {
            Progress = 75;
            break;
          }

          var convertedLine = line.Trim();
          if (convertedLine.StartsWith("<!--")) continue;

          if (convertedLine.StartsWith("<"))
          {
            const string pattern = @"</?\w+((\s+\w+(\s*=\s*(?:"".*?""|'.*?'|[^'"">\s]+))?)+\s*|\s*)/?>";
            var htmlClean = new Regex(pattern, RegexOptions.Singleline);
            convertedLine = htmlClean.Replace(convertedLine, string.Empty);
          }
          if (string.IsNullOrEmpty(convertedLine)) continue;

          if (convertedLine.Contains("&nbsp;"))
            convertedLine = convertedLine.Replace("&nbsp;", string.Empty);

          convertedLine = convertedLine.Surround("<p>", "</p>");

          if (UsePartsRegex && partsRegex.IsMatch(convertedLine))
          {
            if (currentPart != null)
            {
              book.Parts.Add(currentPart);
            }
            currentPart = new Part { Title = convertedLine };
            currentChapter = -1;
            if (!UseChaptersRegex)
            {
              string title = string.Format("<p>Глава {0}</p>", currentPart.Chapters.Count + 1);
              currentPart.Chapters.Add(new Chapter(title));
              currentChapter = 0;
            }
            continue;
          }
          if (UseChaptersRegex && chaptersRegex.IsMatch(convertedLine))
          {
            if (!UsePartsRegex && currentPart == null)
              currentPart = new Part { Title = "<p>" + book.Title + "</p>" };

            if (currentPart != null)
            {
              currentChapter++;
              var chapter = new Chapter(convertedLine);
              currentPart.Chapters.Add(chapter);
              Messages.Notification("Глава '{0}' parsed.", chapter.Title);
              continue;
            }
          }
          if (currentPart == null) continue;
          if (currentChapter < 0) continue;

          currentPart.Chapters[currentChapter].Lines.Add(convertedLine);
        }
        if (currentPart != null)
        {
          book.Parts.Add(currentPart);
        }
      }
      return book;
    }

    /*
<description>
 <title-info>
  <genre>history_russia</genre>
  <author>
   <first-name>Лев</first-name>
   <middle-name>Николаевич</middle-name>
   <last-name>Толстой</last-name>
  </author>
  <book-title>Война и мир</book-title>
  <lang>ru</lang>
 </title-info>
 <document-info>
  <author>
   <nickname>GribUser</nickname>
  </author>
 <date value="2002-10-15">15 ноября 2002г., 19:53</date>
 <id>GribUser_WarAndWorld_D49FHSH8l0HS5</id>
 <version>2.0</version>
 </document-info>
</description>
     */
    private XmlDocument CreateFB2Document(ParsedFicionBook book)
    {
      Messages.Notification("Creating FB2 format.");
      var xmlDoc = new XmlDocument();
      // Write down the XML declaration
      XmlDeclaration xmlDeclaration = xmlDoc.CreateXmlDeclaration("1.0", "windows-1251", null);
      // Create the root element
      XmlElement rootNode = xmlDoc.CreateElement("FictionBook", "http://www.gribuser.ru/xml/fictionbook/2.0");
      xmlDoc.InsertBefore(xmlDeclaration, xmlDoc.DocumentElement);
      xmlDoc.AppendChild(rootNode);

      // Create a new <Category> element and add it to the root node
      XmlElement descriptionNode = xmlDoc.CreateElement("description");
      xmlDoc.DocumentElement.PrependChild(descriptionNode);

      // Create the required nodes
      XmlElement titleNode = xmlDoc.CreateElement("title-info");
      XmlElement genreNode = xmlDoc.CreateElement("genre");
      XmlElement authorNode = xmlDoc.CreateElement("author");
      XmlElement firstNameNode = xmlDoc.CreateElement("first-name");
      firstNameNode.AppendChild(xmlDoc.CreateTextNode(book.First));
      authorNode.AppendChild(firstNameNode);
      XmlElement middleNameNode = xmlDoc.CreateElement("middle-name");
      middleNameNode.AppendChild(xmlDoc.CreateTextNode(book.Middle));
      authorNode.AppendChild(middleNameNode);
      XmlElement lastNameNode = xmlDoc.CreateElement("last-name");
      lastNameNode.AppendChild(xmlDoc.CreateTextNode(book.Last));
      authorNode.AppendChild(lastNameNode);
      XmlElement bookTitleNode = xmlDoc.CreateElement("book-title");
      bookTitleNode.AppendChild(xmlDoc.CreateTextNode(book.Title));
      XmlElement langNode = xmlDoc.CreateElement("lang");
      langNode.AppendChild(xmlDoc.CreateTextNode("RU"));
      titleNode.AppendChild(genreNode);
      titleNode.AppendChild(authorNode);
      titleNode.AppendChild(bookTitleNode);
      titleNode.AppendChild(langNode);

      XmlElement docInfoNode = xmlDoc.CreateElement("document-info");
      XmlElement fb2Creator = xmlDoc.CreateElement("author");
      fb2Creator.AppendChild(xmlDoc.CreateTextNode("starmonkey"));
      docInfoNode.AppendChild(fb2Creator);
      XmlElement dateNode = xmlDoc.CreateElement("date");
      DateTime date = DateTime.Now;
      XmlAttribute dateAttr = xmlDoc.CreateAttribute("value");
      dateAttr.Value = date.ToShortDateString();
      dateNode.Attributes.Append(dateAttr);
      dateNode.AppendChild(xmlDoc.CreateTextNode(date.ToString()));
      XmlElement idNode = xmlDoc.CreateElement("id");
      idNode.AppendChild(xmlDoc.CreateTextNode(Guid.NewGuid().ToString()));
      XmlElement versionNode = xmlDoc.CreateElement("version");
      versionNode.AppendChild(xmlDoc.CreateTextNode("2.0"));
      docInfoNode.AppendChild(dateNode);
      docInfoNode.AppendChild(idNode);
      docInfoNode.AppendChild(versionNode);

      descriptionNode.AppendChild(titleNode);
      descriptionNode.AppendChild(docInfoNode);

      XmlElement bodyNode = xmlDoc.CreateElement("body");
      int allChapters = book.Parts.SelectMany(p => p.Chapters).Count();
      int counter = 0;
      foreach (var part in book.Parts)
      {
        XmlElement partNode = xmlDoc.CreateElement("section");
        foreach (var chapter in part.Chapters)
        {
          XmlElement chapterNode = xmlDoc.CreateElement("section");
          try
          {
            chapterNode.InnerXml = chapter.GetChapterText();
            partNode.AppendChild(chapterNode);
          }
          catch (Exception e)
          {
            Messages.Error("{0}{1}{2}", part.Title, chapter.Title, e.Message);
          }
          Progress = (++counter * 25 / allChapters) + 75;
        }
        bodyNode.AppendChild(partNode);
      }
      xmlDoc.DocumentElement.AppendChild(bodyNode);

      xmlDoc.AppendChild(rootNode);

      return xmlDoc;
    }

    private static int CountLines(TextReader reader)
    {
      var buffer = new char[512 * 1024]; // Read 512K chars at a time
      var total = 1; // All files have at least one line!
      int read;
      while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
        for (var i = 0; i < read; i++)
          if (buffer[i] == '\n')
            total++;
      return total;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string property)
    {
      var handler = PropertyChanged;
      if (handler != null)
        if (_syncContext != SynchronizationContext.Current)
          _syncContext.Send(state => handler(this, new PropertyChangedEventArgs(property)), null);
        else
          handler(this, new PropertyChangedEventArgs(property));
    }

  }

  public static class StringExt
  {
    public static string Surround(this string text, string start, string end)
    {
      return string.Format("{0}{1}{2}", start, text, end);
    }
  }
}